package is.glif.fenius.webui.client;

import is.glif.fenius.util.ConfigManager;
import is.glif.fenius.webui.common.WebUIException;
import is.glif.fenius.webui.db.CompositeReservationDAO;
import is.glif.fenius.webui.db.WebReserveRequestDAO;
import is.glif.fenius.webui.db.bean.Component;
import is.glif.fenius.webui.db.bean.CompositeReservation;
import is.glif.fenius.webui.db.bean.WebReserveRequest;
import is.glif.fenius.ws.connection.gen.Advance;
import is.glif.fenius.ws.connection.gen.Avp;
import is.glif.fenius.ws.connection.gen.AvpSeq;
import is.glif.fenius.ws.connection.gen.Connection;
import is.glif.fenius.ws.connection.gen.ConnectionService;
import is.glif.fenius.ws.connection.gen.Edge;
import is.glif.fenius.ws.connection.gen.ExternalFault_Exception;
import is.glif.fenius.ws.connection.gen.InternalFault_Exception;
import is.glif.fenius.ws.connection.gen.ListFilter;
import is.glif.fenius.ws.connection.gen.ListReq;
import is.glif.fenius.ws.connection.gen.ListResp;
import is.glif.fenius.ws.connection.gen.ListResponse;
import is.glif.fenius.ws.connection.gen.Reservable;
import is.glif.fenius.ws.connection.gen.Reservation;
import is.glif.fenius.ws.connection.gen.Reserve;
import is.glif.fenius.ws.connection.gen.ReserveReq;
import is.glif.fenius.ws.connection.gen.ReserveResponse;
import is.glif.fenius.ws.connection.gen.Schedule;
import is.glif.fenius.ws.connection.gen.Topology;
import is.glif.fenius.ws.connection.gen.Vertex;
import java.io.FileInputStream;
import java.net.Authenticator;
import java.net.MalformedURLException;
import java.net.PasswordAuthentication;
import java.net.URL;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;
import javax.xml.ws.BindingProvider;

import org.apache.cxf.configuration.jsse.TLSClientParameters;
import org.apache.cxf.configuration.security.FiltersType;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.interceptor.LoggingInInterceptor;
import org.apache.cxf.interceptor.LoggingOutInterceptor;
import org.apache.cxf.transport.http.HTTPConduit;
import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
import org.apache.log4j.Logger;
import org.json.JSONException;


public class Requester {
    private static Logger log = Logger.getLogger(Requester.class);
    private static Requester instance;
    private String sslTruststorePath;
    private String sslKeystorePath;
    private String sslTruststorePass;
    private String sslKeystorePass;
    private String sslKeyPass;
    private boolean sslCheckHostnames;
    private boolean sendRequests;
    private Long httpTimeoutMs;
    private Map<String, Object> config;
    private static final QName SERVICE_NAME = new QName("http://fenius.glif.is/ws/connection/v1/service", "connection");

    
    public static Requester getInstance() {
        if (instance == null) {
            instance = new Requester();
        }
        return instance;
        
    }
 
    private Requester() {
        configure();
    }
    
    
    private void configure() {
        ConfigManager cm = ConfigManager.getInstance();
        this.config = cm.getConfiguration();
        
        this.sslKeystorePath = (String) this.config.get("sslKeystorePath");
        this.sslKeystorePass = (String) this.config.get("sslKeystorePass");
        this.sslKeyPass = (String) this.config.get("sslKeyPass");
        this.sslTruststorePath = (String) this.config.get("sslTruststorePath");
        this.sslTruststorePass = (String) this.config.get("sslTruststorePass");
        
        this.sslCheckHostnames = (Boolean) this.config.get("sslCheckHostnames");
        this.sendRequests = (Boolean) this.config.get("sendRequests");
        Integer timeoutMs = (Integer) this.config.get("httpTimeoutMs");
        this.httpTimeoutMs = timeoutMs.longValue();
        log.debug("keystore path:"+sslKeystorePath);
        log.debug("keystore pass:"+sslKeystorePass);
        log.debug("truststore path:"+sslTruststorePath);
        log.debug("truststore pass:"+sslTruststorePass);
    }
    
    public void reserve(CompositeReservation resv) throws WebUIException {
        FileCredentialsProvider cp = FileCredentialsProvider.getInstance();
        WebReserveRequest req = resv.getWebReserveRequest();
        CompositeReservationDAO dao = new CompositeReservationDAO();
        WebReserveRequestDAO reqDao = new WebReserveRequestDAO();

        try {
            for (Component comp : resv.getComponents()) {
                Reservable res = this.makeReservable(comp, req);
                if (!this.sendRequests) {
                    Random rand = new Random();
                    String resvId = "resv-"+Math.abs(rand.nextInt());
                    log.info("not sending reserve request because of config");
                    comp.setReservationId(resvId);
                } else {
                    String netId = comp.getNetworkId();
                    String url = cp.getURL(netId);
                    Boolean ssl = cp.isSSL(netId);
                    Boolean httpBasic = cp.isHttpBasicAuth(netId);
                    String pwd = cp.getPassword(netId);
                    String user = cp.getUsername(netId);
                    log.debug(netId+" "+url+" "+user+" "+pwd+" ssl: "+ssl+" http-basic: "+httpBasic);
                    
                    ConnectionService port = this.initSoap(url, user, pwd, httpBasic, ssl);
                    log.info("sending reserve() to "+netId+"...");
                    Reservation result = this.sendReserve(port, res);
                    if (result != null) {
                        log.info("reserve() : got reservation id: "+result.getReservationId());
                        comp.setReservationId(result.getReservationId());
                    }
                }
            }
        } catch (JSONException e) {
            log.error("JSON exception!", e);
        }
        log.debug("saving composite reservation..");
        //reqDao.create(req);
        dao.create(resv);
    }
    
    public Reservation sendReserve(ConnectionService port, Reservable res) {
        Reserve reserve = new Reserve();
        ReserveReq req = new ReserveReq();
        req.setReservationParams(res);
        reserve.setArg0(req);
        try {
            ReserveResponse resp = port.reserve(reserve);
            Reservation resv = resp.getReturn().getReservation();
            log.debug("resv id: "+resv.getReservationId());
            return resv;
        } catch (InternalFault_Exception e) {
            log.error("Internal error!", e);
            e.printStackTrace();
        } catch (ExternalFault_Exception e) {
            log.error("External error!", e);
            e.printStackTrace();
        }
        return null;
    }
    
    
    public HashMap<String, List<Reservation>> listAllCurrent(List<String> networkIds) throws WebUIException {
        FileCredentialsProvider cp = FileCredentialsProvider.getInstance();
        HashMap<String, List<Reservation>> resvsByNetwork = new HashMap<String, List<Reservation>>();
        
        try {
            for (String netId : networkIds) {
                String url = cp.getURL(netId);
                Boolean ssl = cp.isSSL(netId);
                Boolean httpBasic = cp.isHttpBasicAuth(netId);
                String pwd = cp.getPassword(netId);
                String user = cp.getUsername(netId);
                log.debug(netId+" "+url+" "+user+" "+pwd+" ssl: "+ssl+" http-basic: "+httpBasic);
                
                ListFilter fil = new ListFilter();
                
                GregorianCalendar gcNow = (GregorianCalendar) GregorianCalendar.getInstance();
                gcNow.setTime(new Date());
                
                DatatypeFactory factory = null;
                try {
                    factory = DatatypeFactory.newInstance();
                } catch (DatatypeConfigurationException dce) {
                    throw new WebUIException(dce.getMessage());
                }
                
                XMLGregorianCalendar xmlNow = factory.newXMLGregorianCalendar(gcNow);
                fil.setEndsAfter(xmlNow);
                
                ConnectionService port = this.initSoap(url, user, pwd, httpBasic, ssl);
                List<Reservation> resvs = this.sendList(port, fil);
                resvsByNetwork.put(netId, resvs);
            }
        } catch (JSONException e) {
            log.error("JSON exception!", e);
        }
        return resvsByNetwork;
    }
    
    
    public List<Reservation> sendList(ConnectionService port, ListFilter fil) {
        
        ListReq req = new ListReq();
        req.setListFilter(fil);
        is.glif.fenius.ws.connection.gen.List list = new is.glif.fenius.ws.connection.gen.List();
        list.setArg0(req);
    
        try {
            ListResponse outer = port.list(list);
            ListResp inner = outer.getReturn();
            return inner.getReservations();
            
        } catch (InternalFault_Exception e) {
            log.error("Internal error!", e);
            e.printStackTrace();
        } catch (ExternalFault_Exception e) {
            log.error("External error!", e);
            e.printStackTrace();
        } catch (Exception e) {
            log.error("Miscellaneous error!", e);
            e.printStackTrace();
            
        }
        
        return null;
        
    }
    

    private Reservable makeReservable(Component comp, WebReserveRequest req) throws WebUIException {
        String aResourceId = comp.getEndpointA();
        String zResourceId = comp.getEndpointZ();
        Integer bandwidth = req.getBandwidth();
        Long upMillis = req.getUpMillis();
        Long dnMillis = req.getDnMillis();
        Integer srcVlan = req.getSrcVlan();
        Integer dstVlan = req.getDstVlan();
        
        log.debug("a: "+aResourceId);
        log.debug("z: "+zResourceId);
        log.debug("bw: "+bandwidth);
        log.debug("up: "+upMillis);
        log.debug("dn: "+dnMillis);
        log.debug("sv: "+srcVlan);
        log.debug("dv: "+dstVlan);
        
        
        
        Reservable res = new Reservable();
        
        // set topology
        Topology topo = new Topology();
        topo.setDirected(false);

        Vertex a = new Vertex();
        a.setVertexId("vertexA");
        a.setResourceId(aResourceId);
        Vertex z = new Vertex();
        z.setVertexId("vertexZ");
        z.setResourceId(zResourceId);

        topo.getVertices().add(a);
        topo.getVertices().add(z);
        
        Edge e = new Edge();
        e.setA("vertexA");
        e.setBandwidth(bandwidth);
        e.setZ("vertexZ");
        topo.getEdges().add(e);
        
        res.setTopology(topo);

        
        // set ethernet params
        Avp srcVlanValuePair = new Avp();
        srcVlanValuePair.setAttrName("eth.vlanTag");
        srcVlanValuePair.setAttrVal(srcVlan.toString());
        AvpSeq aTechParams = new AvpSeq();
        aTechParams.getAvp().add(srcVlanValuePair);
        a.setTechParams(aTechParams);
        
        
        Avp dstVlanValuePair = new Avp();
        dstVlanValuePair.setAttrName("eth.vlanTag");
        dstVlanValuePair.setAttrVal(dstVlan.toString());
        AvpSeq zTechParams = new AvpSeq();
        zTechParams.getAvp().add(dstVlanValuePair);
        z.setTechParams(zTechParams);
        
        
        
        // set schedule
        Long durationMillis =  dnMillis - upMillis;
        Integer duration = durationMillis.intValue() / 1000;
        
        GregorianCalendar gcNow = (GregorianCalendar) GregorianCalendar.getInstance();

        GregorianCalendar gcUp = (GregorianCalendar) GregorianCalendar.getInstance();
        gcUp.setTimeInMillis(upMillis);
        DatatypeFactory factory = null;
        try {
            factory = DatatypeFactory.newInstance();
        } catch (DatatypeConfigurationException dce) {
            throw new WebUIException(dce.getMessage());
        }
        
        XMLGregorianCalendar xmlNow = factory.newXMLGregorianCalendar(gcNow);
        XMLGregorianCalendar xmlUp = factory.newXMLGregorianCalendar(gcUp);


        Advance adv = new Advance();
        adv.setStart(xmlUp);
        adv.setDuration(duration);

        Schedule sched = new Schedule();
        sched.setAdvance(adv);
        sched.setCreated(xmlNow);
        res.setSchedule(sched);

        
        res.getDescription().add("Fenius reservation");
        
        return res;

    }

    private ConnectionService initSoap(String url, String user, String pwd, boolean httpBasic, boolean ssl) throws WebUIException {

        String wsdlLocation = url;

        URL wsdlURL = null;
        try {
            wsdlURL = new URL(wsdlLocation);
        } catch (MalformedURLException ex) {
            log.error(ex);
            throw new WebUIException("Internal error");
        }
        
        if (ssl) {
            configureSSLinJVM(wsdlURL);
        }
        
        // before 
        if (!sslCheckHostnames) {
            HttpsURLConnection.setDefaultHostnameVerifier(new NullHostnameVerifier());
        }
        if (httpBasic) {
            Authenticator.setDefault(new MyAuthenticator(user, pwd));        
        }

        
        System.out.println(wsdlLocation);
        Connection ss = new Connection(wsdlURL, SERVICE_NAME);

//        ConnectionProxy ss = new ConnectionProxy(wsdlURL);
        ConnectionService port = ss.getConnectionServicePort();
        
        Client client = ClientProxy.getClient(port);
        client.getInInterceptors().add(new LoggingInInterceptor());
        client.getOutInterceptors().add(new LoggingOutInterceptor()); 
        
        HTTPConduit httpConduit = (HTTPConduit) client.getConduit();
        HTTPClientPolicy cp = new HTTPClientPolicy();
        
        Long connTO = httpTimeoutMs;
        Long rcvTO = httpTimeoutMs;
        
        cp.setConnectionTimeout(connTO);
        cp.setReceiveTimeout(rcvTO);
        httpConduit.setClient(cp);
        
        if (ssl) {
            TLSClientParameters tlsParams = new TLSClientParameters(); 
            if (sslCheckHostnames) {
                tlsParams.setDisableCNCheck(false); 
            } else {
                tlsParams.setDisableCNCheck(true); 
            }
            FiltersType filter = new FiltersType(); 
            filter.getInclude().add(".*_EXPORT_.*"); 
            filter.getInclude().add(".*_EXPORT1024_.*"); 
            filter.getInclude().add(".*_WITH_DES_.*"); 
            filter.getInclude().add(".*_WITH_3DES_.*"); 
            filter.getInclude().add(".*_WITH_NULL_.*"); 
            filter.getExclude().add(".*_DH_anon_.*"); 
            tlsParams.setCipherSuitesFilter(filter); 
            
            httpConduit.setTlsClientParameters(tlsParams);             
        }
        if (httpBasic) {
            BindingProvider bp = (BindingProvider) port;
            bp.getRequestContext().put(BindingProvider.USERNAME_PROPERTY, user);
            bp.getRequestContext().put(BindingProvider.PASSWORD_PROPERTY, pwd);
        }
        return port;
    }
    
    private void configureSSLinJVM(URL url) {
        log.debug("truststore: "+sslTruststorePath);
        System.setProperty("javax.net.ssl.trustStoreType", "jks");  
        System.setProperty("javax.net.ssl.trustStore", sslTruststorePath);  
        System.setProperty("javax.net.ssl.trustStorePassword", sslTruststorePass);  
          
        System.setProperty("javax.net.ssl.keyStoreType", "jks");  
        System.setProperty("javax.net.ssl.keyStore", sslKeystorePath);  
        System.setProperty("javax.net.ssl.keyStorePassword",sslKeystorePass);  
        try {
            SSLContext ctx = SSLContext.getInstance("TLS");
            char[] keyStorePassword = sslKeystorePass.toCharArray();
            char[] privateKeyPassword = sslKeyPass.toCharArray();
            KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
            KeyStore ks = KeyStore.getInstance("JKS");
            ks.load(new FileInputStream(sslKeystorePath), keyStorePassword);
            kmf.init(ks, privateKeyPassword);
            ctx.init(kmf.getKeyManagers(), null, null);
            SSLSocketFactory sf = ctx.getSocketFactory();
            HttpsURLConnection.setDefaultSSLSocketFactory(sf);
            SSLSocket socket = (SSLSocket) sf.createSocket(url.getHost(), url.getPort());
            String[] suites = socket.getSupportedCipherSuites();
            socket.setEnabledCipherSuites(suites);

        } catch (Exception ex) {
            log.error(ex);
        }

    }
    public void setSendRequests(boolean sendRequests) {
        this.sendRequests = sendRequests;
    }

    public boolean isSendRequests() {
        return sendRequests;
    }
    private static class MyAuthenticator extends Authenticator {
        String user;
        String pwd;
        public MyAuthenticator(String user, String pwd) {
            this.user = user;
            this.pwd = pwd;
        }
        @Override
        protected PasswordAuthentication getPasswordAuthentication() {
            log.debug("sending "+user+" "+pwd);
            return new PasswordAuthentication(user, pwd.toCharArray());
        }
    }
    private static class NullHostnameVerifier implements HostnameVerifier {
        public boolean verify(String hostname, SSLSession session) {
            return true;
        }
    }
    
    
    /*

    private void create() {
        SimpleDateFormat df = new SimpleDateFormat("yyyyMMddhhmmss");
        GregorianCalendar now = (GregorianCalendar) GregorianCalendar.getInstance();
        df.setTimeZone(TimeZone.getTimeZone("GMT"));

        System.out.println("Processing request set: "+requestSetId);
        String reservationId = "fenius-"+df.format(now.getTime());
        RequestSet rs = clientConfig.getRequestSet(requestSetId);

        String strBw = rs.getBandwidth();
        Integer bandwidth = Integer.valueOf(strBw);

        GregorianCalendar gcStart = (GregorianCalendar) GregorianCalendar.getInstance();
        String strStartTime = rs.getStartTime();
        XMLGregorianCalendar startTime = null;
        if (strStartTime.equals("now")) {
            startTime = new XMLGregorianCalendarImpl(gcStart);
        } else {
            System.out.println("Only support \"now\" for start time at the moment");
            System.exit(1);
        }

        XMLGregorianCalendar endTime = null;
        GregorianCalendar gcEnd = (GregorianCalendar) GregorianCalendar.getInstance();
        String strEndTime = rs.getEndTime();
        if (strEndTime.startsWith("+")) {
            strEndTime = strEndTime.substring(1);
            Integer duration = Integer.valueOf(strEndTime);
            Long millisStart = gcStart.getTimeInMillis();
            Long millisEnd = millisStart + (duration * 1000);
            gcEnd.setTimeInMillis(millisEnd);
            endTime = new XMLGregorianCalendarImpl(gcEnd);
        } else {
            System.out.println("Only support \"+XXX\" format for end time at the moment");
            System.exit(1);
        }


        for (String rid : rs.getRequestIds()) {
            System.out.println(" Processing request: "+rid);
            Request req = clientConfig.getRequest(rid);
            String vlan = req.getVlan();

            String src = clientConfig.getEndpointInfo(req.getSrc()).getUrn();
            String dst = clientConfig.getEndpointInfo(req.getDst()).getUrn();
            EndpointPair ep = new EndpointPair();

            String serverId = req.getServerId();

            ReservationParams resvParams = new ReservationParams();
            CommonParams cp = new CommonParams();
            TimeParams tp = new TimeParams();
            AvpSeq adhoc = new AvpSeq();
            Avp vlanParam = new Avp();


            cp.setStatus(Status.REQUESTING);
            tp.setRequestedStartTime(startTime);
            tp.setRequestedEndTime(endTime);
            vlanParam.setAttrName("eth.vlan");
            vlanParam.setAttrVal(vlan);
            ep.setA(src);
            ep.setZ(dst);
            cp.setBandwidth(bandwidth);

            resvParams.setReservationId(reservationId);
            resvParams.setEndpoints(ep);
            resvParams.setEndtoendParams(cp);
            cp.setTimeParams(tp);
            cp.setAdhocParams(adhoc);
            adhoc.getAvp().add(vlanParam);
            reservations.put(rid+"-requested", resvParams);
            this.sendCreate(serverId, rid, resvParams);
        }
    }


    private void sendCreate(String serverId, String requestId, ReservationParams resvParams) {
        this.initSoap(serverId);
        Create wrapReq = new Create();
        CreateReq createReq = new CreateReq();
        wrapReq.setArg0(createReq);
        createReq.setResvParams(resvParams);
        try {
            CreateResponse wrapResp = port.create(wrapReq);
            CreateResp createResp = wrapResp.getReturn();
            reservations.put(requestId+"-result", createResp.getResvParams());

        } catch (InternalFault_Exception e) {
            System.out.println("InternalFault has occurred.");
            System.out.println(e.toString());
        } catch (ExternalFault_Exception e) {
            System.out.println("ExternalFault has occurred.");
            System.out.println(e.toString());
        }
    }



    private void list() {
        this.initSoap(serverId);

        is.glif.fenius.ws.draft.gen.List wrapReq = new is.glif.fenius.ws.draft.gen.List();
        ListReq listReq = new ListReq();
        wrapReq.setArg0(listReq);
        try {
            ListResponse resp = port.list(wrapReq);
            List<ReservationParams> resvs = resp.getReturn().getReservations();
            for (ReservationParams resv : resvs) {
                System.out.println("got id: "+resv.getReservationId());
                reservations.put(resv.getReservationId(), resv);
            }

        } catch (InternalFault_Exception e) {
            System.out.println("InternalFault has occurred.");
            e.printStackTrace();
            System.out.println(e.toString());
        } catch (ExternalFault_Exception e) {
            System.out.println("ExternalFault has occurred.");
            e.printStackTrace();
            System.out.println(e.toString());
        }
    }


    private void cancel() {
        System.out.println("Not implemented yet");
    }

*/






}
